<!DOCTYPE html>
<html>
<head>
  <title>Grafcet Diagrams</title>
  <meta name="description" content="A Grafcet diagram editor, showing buttons for creating new nodes and links related to the selected node." />
  <!-- Copyright 1998-2016 by Northwoods Software Corporation. -->
  <meta charset="UTF-8">
  <script src="go.js"></script>
  <link href="../assets/css/goSamples.css" rel="stylesheet" type="text/css" />  <!-- you don't need to use this -->
  <script src="goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->
  <script id="code">
  function init() {
    if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
    var $ = go.GraphObject.make;

    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          allowLink: false,  // linking is only started via buttons, not modelessly;
                             // see the "startLink..." functions and CustomLinkingTool defined below
          initialContentAlignment: go.Spot.Center,
          // double-click in the background creates a new "Start" node
          "clickCreatingTool.archetypeNodeData": { category: "Start", step: 1, text: "Action" },
          linkingTool: new CustomLinkingTool(),  // defined below to automatically turn on allowLink
          "undoManager.isEnabled": true
        });

    // when the document is modified, add a "*" to the title and enable the "Save" button
    myDiagram.addDiagramListener("Modified", function(e) {
      var button = document.getElementById("saveModel");
      if (button) button.disabled = !myDiagram.isModified;
      var idx = document.title.indexOf("*");
      if (myDiagram.isModified) {
        if (idx < 0) document.title += "*";
      } else {
        if (idx >= 0) document.title = document.title.substr(0, idx);
      }
    });

    // This implements a selection Adornment that is a horizontal bar of command buttons
    // that appear when the user selects a node.
    // Each button has a click function to execute the command, a tooltip for a textual description,
    // and a Binding of "visible" to hide the button if it cannot be executed for that particular node.

    var commandsAdornment =
      $(go.Adornment, "Vertical",
        $(go.Panel, "Auto",
          $(go.Shape, { fill: null, stroke: "deepskyblue", strokeWidth: 2 }),
          $(go.Placeholder)
        ),
        $(go.Panel, "Horizontal",
          { defaultStretch: go.GraphObject.Vertical },
          $("Button",
            $(go.Shape,
              { geometryString: "M0 0 L10 0",
                fill: null, stroke: "red", margin: 3 }),
            { click: addExclusive, toolTip: makeTooltip("Add Exclusive") },
            new go.Binding("visible", "", canAddSplit).ofObject()),
          $("Button",
            $(go.Shape,
              { geometryString: "M0 0 L10 0 M0 3 10 3",
                fill: null, stroke: "red", margin: 3 }),
            { click: addParallel, toolTip: makeTooltip("Add Parallel") },
            new go.Binding("visible", "", canAddSplit).ofObject()),
          $("Button",
            $(go.Shape,
              { geometryString: "M0 0 L10 0 10 6 0 6z",
                fill: "lightyellow", margin: 3 }),
            { click: addStep, toolTip: makeTooltip("Add Step") },
            new go.Binding("visible", "", canAddStep).ofObject()),
          $("Button",
            $(go.Shape,
              { geometryString: "M0 0 M5 0 L5 10 M3 8 5 10 7 8 M10 0",
                fill: null, margin: 3 }),
            { click: startLinkDown, toolTip: makeTooltip("Draw Link Down") },
            new go.Binding("visible", "", canStartLink).ofObject()),
          $("Button",
            $(go.Shape,
              { geometryString: "M0 0 M3 0 L3 2 7 2 7 6 3 6 3 10 M1 8 3 10 5 8 M10 0",
                fill: null, margin: 3 }),
            { click: startLinkAround, toolTip: makeTooltip("Draw Link Skip") },
            new go.Binding("visible", "", canStartLink).ofObject()),
          $("Button",
            $(go.Shape,
              { geometryString: "M0 0 M3 2 L3 0 7 0 7 10 3 10 3 8 M5 6 7 4 9 6 M10 0",
                fill: null, margin: 3 }),
            { click: startLinkUp, toolTip: makeTooltip("Draw Link Repeat") },
            new go.Binding("visible", "", canStartLink).ofObject())
        )
      );

    function makeTooltip(str) {  // a helper function for defining tooltips for buttons
      return $(go.Adornment, go.Panel.Auto,
               $(go.Shape, { fill: "#FFFFCC" }),
               $(go.TextBlock, str, { margin: 4 }));
    }

    // Commands for adding new Nodes

    function addStep(e, obj) {
      var node = obj.part.adornedPart;
      var model = myDiagram.model;
      model.startTransaction("add Step");
      var loc = node.location.copy();
      loc.y += 50;
      var nodedata = { location: go.Point.stringify(loc) };
      model.addNodeData(nodedata);
      var nodekey = model.getKeyForNodeData(nodedata);
      var linkdata = { from: model.getKeyForNodeData(node.data), to: nodekey, text: "c" };
      model.addLinkData(linkdata);
      var newnode = myDiagram.findNodeForData(nodedata);
      myDiagram.select(newnode);
      model.commitTransaction("add Step");
    }

    function canAddStep(adorn) {
      var node = adorn.adornedPart;
      if (node.category === "" || node.category === "Start") {
        return node.findLinksOutOf().count === 0;
      } else if (node.category === "Parallel" || node.category === "Exclusive") {
        return true;
      }
      return false;
    }

    function addParallel(e, obj) { addSplit(obj.part.adornedPart, "Parallel"); }
    function addExclusive(e, obj) { addSplit(obj.part.adornedPart, "Exclusive"); }

    function addSplit(node, type) {
      var model = myDiagram.model;
      model.startTransaction("add " + type);
      var loc = node.location.copy();
      loc.y += 50;
      var nodedata = { category: type, location: go.Point.stringify(loc) };
      model.addNodeData(nodedata);
      var nodekey = model.getKeyForNodeData(nodedata);
      var linkdata = { from: model.getKeyForNodeData(node.data), to: nodekey };
      model.addLinkData(linkdata);
      var newnode = myDiagram.findNodeForData(nodedata);
      myDiagram.select(newnode);
      model.commitTransaction("add " + type);
    }

    function canAddSplit(adorn) {
      var node = adorn.adornedPart;
      if (node.category === "" || node.category === "Start") {
        return node.findLinksOutOf().count === 0;
      } else if (node.category === "Parallel" || node.category === "Exclusive") {
        return false;
      }
      return false;
    }

    // Commands for starting drawing new Links

    function startLinkDown(e, obj) { startLink(obj.part.adornedPart, "", "c"); }
    function startLinkAround(e, obj) { startLink(obj.part.adornedPart, "Skip", "s"); }
    function startLinkUp(e, obj) { startLink(obj.part.adornedPart, "Repeat", "r"); }

    function startLink(node, category, condition) {
      var tool = myDiagram.toolManager.linkingTool;
      // to control what kind of Link is created,
      // change the LinkingTool.archetypeLinkData's category
      myDiagram.model.setCategoryForLinkData(tool.archetypeLinkData, category);
      // also change the text indicating the condition, which the user can edit
      tool.archetypeLinkData.text = condition;
      tool.startObject = node.port;
      myDiagram.currentTool = tool;
      tool.doActivate();
    }

    function canStartLink(adorn) {
      var node = adorn.adornedPart;
      return true;  // this could be smarter
    }


    // The various kinds of Nodes

    // a helper function that declares common properties for all kinds of nodes
    function commonNodeStyle() {
      return [
        {
          locationSpot: go.Spot.Center,
          selectionAdornmentTemplate: commandsAdornment  // shared selection Adornment
        },
        new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify),
      ];
    }

    myDiagram.nodeTemplateMap.add("Start",
      $(go.Node, "Horizontal", commonNodeStyle(),
        { locationObjectName: "STEPPANEL", selectionObjectName: "STEPPANEL" },
        $(go.Panel, "Auto",
          { // this is the port element, not the whole Node
            name: "STEPPANEL", portId: "",
            fromSpot: go.Spot.Bottom, fromLinkable: true
          },
          $(go.Shape, { fill: "lightgreen" }),
          $(go.Panel, "Auto",
            { margin: 3 },
            $(go.Shape, { fill: null, minSize: new go.Size(20, 20) }),
            $(go.TextBlock, "Start",
              { margin: 3, editable: true },
              new go.Binding("text", "step").makeTwoWay())
          )
        ),
        // a connector line between the texts
        $(go.Shape, "LineH", { width: 10, height: 1 }),
        // the boxed, editable text on the side
        $(go.Panel, "Auto",
          $(go.Shape, { fill: "white" }),
          $(go.TextBlock, "Action",
            { margin: 3, editable: true },
            new go.Binding("text", "text").makeTwoWay())
        )
      ));

    myDiagram.nodeTemplateMap.add("",
      $(go.Node, "Horizontal", commonNodeStyle(),
        { locationObjectName: "STEPPANEL", selectionObjectName: "STEPPANEL" },
        $(go.Panel, "Auto",
          { // this is the port element, not the whole Node
            name: "STEPPANEL", portId: "",
            fromSpot: go.Spot.Bottom, fromLinkable: true,
            toSpot: go.Spot.Top, toLinkable: true
          },
          $(go.Shape, { fill: "lightyellow", minSize: new go.Size(20, 20) }),
          $(go.TextBlock, "Step",
            { margin: 3, editable: true },
            new go.Binding("text", "step").makeTwoWay())
        ),
        $(go.Shape, "LineH", { width: 10, height: 1 }),
        $(go.Panel, "Auto",
          $(go.Shape, { fill: "white" }),
          $(go.TextBlock, "Action",
            { margin: 3, editable: true },
            new go.Binding("text", "text").makeTwoWay())
        )
      ));

    var resizeAdornment =
      $(go.Adornment, go.Panel.Spot,
        $(go.Placeholder),
        $(go.Shape,  // left resize handle
          { alignment: go.Spot.Left, cursor: "col-resize",
            desiredSize: new go.Size(6, 6), fill: "lightblue", stroke: "dodgerblue" }),
        $(go.Shape,  // right resize handle
          { alignment: go.Spot.Right, cursor: "col-resize",
            desiredSize: new go.Size(6, 6), fill: "lightblue", stroke: "dodgerblue" })
      );

    myDiagram.nodeTemplateMap.add("Parallel",
      $(go.Node, commonNodeStyle(),
        { // special resizing: just at the ends
          resizable: true, resizeObjectName: "SHAPE", resizeAdornmentTemplate: resizeAdornment,
          fromLinkable: true, toLinkable: true
        },
        $(go.Shape,
          { // horizontal pair of lines stretched to an initial width of 200
            name: "SHAPE", geometryString: "M0 0 L100 0 M0 4 L100 4",
            fill: "transparent", stroke: "red", width: 200
          },
          new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify))
      ));

    myDiagram.nodeTemplateMap.add("Exclusive",
      $(go.Node, commonNodeStyle(),
        { // special resizing: just at the ends
          resizable: true, resizeObjectName: "SHAPE", resizeAdornmentTemplate: resizeAdornment,
          fromLinkable: true, toLinkable: true
        },
        $(go.Shape,
          { // horizontal line stretched to an initial width of 200
            name: "SHAPE", geometryString: "M0 0 L100 0",
            fill: "transparent", stroke: "red", width: 200
          },
          new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify))
      ));

    // the various kinds of Links

    myDiagram.linkTemplateMap.add("",
      $(BarLink,  // subclass defined below
        { routing: go.Link.Orthogonal },
        $(go.Shape,
          { strokeWidth: 1.5 }),
        $(go.Shape, "LineH",  // only visible when there is text
          { width: 20, height: 1, visible: false },
          new go.Binding("visible", "text", function(t) { return t !== ""; })),
        $(go.TextBlock,  // only visible when there is text
          { alignmentFocus: new go.Spot(0, 0.5, -12, 0), editable: true },
          new go.Binding("text", "text").makeTwoWay(),
          new go.Binding("visible", "text", function(t) { return t !== ""; }))
      ));

    myDiagram.linkTemplateMap.add("Skip",
      $(go.Link,
        {
          routing: go.Link.AvoidsNodes,
          fromSpot: go.Spot.Bottom, toSpot: go.Spot.Top,
          fromEndSegmentLength: 4, toEndSegmentLength: 4
        },
        $(go.Shape,
          { strokeWidth: 1.5 }),
        $(go.Shape, "LineH",  // only visible when there is text
          { width: 20, height: 1, visible: false },
          new go.Binding("visible", "text", function(t) { return t !== ""; })),
        $(go.TextBlock,  // only visible when there is text
          { alignmentFocus: new go.Spot(1, 0.5, 12, 0), editable: true },
          new go.Binding("text", "text").makeTwoWay(),
          new go.Binding("visible", "text", function(t) { return t !== ""; }))
      ));

    myDiagram.linkTemplateMap.add("Repeat",
      $(go.Link,
        {
          routing: go.Link.AvoidsNodes,
          fromSpot: go.Spot.Bottom, toSpot: go.Spot.Top,
          fromEndSegmentLength: 4, toEndSegmentLength: 4
        },
        $(go.Shape,
          { strokeWidth: 1.5 }),
        $(go.Shape,
          { toArrow: "OpenTriangle", segmentIndex: 3, segmentFraction: 0.75 }),
        $(go.Shape,
          { toArrow: "OpenTriangle", segmentIndex: 3, segmentFraction: 0.25 }),
        $(go.Shape, "LineH",  // only visible when there is text
          { width: 20, height: 1, visible: false },
          new go.Binding("visible", "text", function(t) { return t !== ""; })),
        $(go.TextBlock,  // only visible when there is text
          { alignmentFocus: new go.Spot(1, 0.5, 12, 0), editable: true },
          new go.Binding("text", "text").makeTwoWay(),
          new go.Binding("visible", "text", function(t) { return t !== ""; }))
      ));

    // start off with a simple diagram
    load();
  }


  // This custom LinkingTool just turns on Diagram.allowLink when it starts,
  // and turns it off again when it stops so that users cannot draw new links modelessly.
  function CustomLinkingTool() {
    go.LinkingTool.call(this);
  }
  go.Diagram.inherit(CustomLinkingTool, go.LinkingTool);

  // user-drawn linking is normally disabled,
  // but needs to be turned on when using this tool
  /** @override */
  CustomLinkingTool.prototype.doStart = function() {
    this.diagram.allowLink = true;
    go.LinkingTool.prototype.doStart.call(this);
  };

  /** @override */
  CustomLinkingTool.prototype.doStop = function() {
    go.LinkingTool.prototype.doStop.call(this);
    this.diagram.allowLink = false;
  };
  // end CustomLinkingTool


  // This custom Link class is smart about computing the link point and direction
  // at "Parallel" and "Exclusive" nodes.
  function BarLink() {
    go.Link.call(this);
  }
  go.Diagram.inherit(BarLink, go.Link);

  /** @override */
  BarLink.prototype.getLinkPoint = function(node, port, spot, from, ortho, othernode, otherport) {
    var r = new go.Rect(port.getDocumentPoint(go.Spot.TopLeft),
                        port.getDocumentPoint(go.Spot.BottomRight));
    var op = otherport.getDocumentPoint(go.Spot.Center);
    var below = op.y > r.centerY;
    var y = below ? r.bottom : r.top;
    if (node.category === "Parallel" || node.category === "Exclusive") {
      if (op.x < r.left) return new go.Point(r.left, y);
      if (op.x > r.right) return new go.Point(r.right, y);
      return new go.Point(op.x, y);
    } else {
      return new go.Point(r.centerX, y);
    }
  };

  /** @override */
  BarLink.prototype.getLinkDirection = function(node, port, linkpoint, spot, from, ortho, othernode, otherport) {
    var p = port.getDocumentPoint(go.Spot.Center);
    var op = otherport.getDocumentPoint(go.Spot.Center);
    var below = op.y > p.y;
    return below ? 90 : 270;
  };
  // end BarLink class

  // save a model to and load a model from JSON text, displayed below the Diagram
  function save() {
    document.getElementById("mySavedModel").value = myDiagram.model.toJson();
    myDiagram.isModified = false;
  }
  function load() {
    myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
  }
</script>
</head>
<body onload="init()">
<div id="sample">
  <h3>4 Grafcet Diagrams</h3>
  <div id="myDiagramDiv" style="border: solid 1px black; width: 800px; height: 600px"></div>
  <p>
    A grafcet diagram is similar to a <a href="sequentialFunction.html">sequential function chart</a>.
  </p>
  <p>
    Select a Node to show a list of Buttons that enable creating new Nodes or drawing new Links.
    These buttons are defined as an adornment that is used in a common <a>Part.selectionAdornmentTemplate</a>.
    This diagram uses many custom functions, including an overridden <a>LinkingTool</a> and a special
    Link class, <b>BarLink</b>.
  </p>
  <div id="buttons">
    <button id="saveModel" onclick="save()">Save</button>
    <button id="loadModel" onclick="load()">Load</button>
    Diagram Model saved in JSON format:
  </div>
  <textarea id="mySavedModel" style="width:100%;height:300px">
{ "class": "go.GraphLinksModel",
  "nodeDataArray": [
{"key":1, "category":"Start", "location":"300 50", "step":"1", "text":"Action 1"},
{"key":2, "category":"Parallel", "location":"300 100"},
{"key":3, "location":"225 125", "step":"3", "text":"Action 2"},
{"key":4, "location":"325 150", "step":"4", "text":"Action 3"},
{"key":5, "location":"225 175", "step":"5", "text":"Action 4"},
{"key":6, "category":"Parallel", "location":"300 200"},
{"key":7, "location":"300 250", "step":"7", "text":"Action 6"},
{"key":11, "category":"Start", "location":"300 350", "step":"11", "text":"Action 1"},
{"key":12, "category":"Exclusive", "location":"300 400"},
{"key":13, "location":"225 450", "step":"13", "text":"Action 2"},
{"key":14, "location":"325 475", "step":"14", "text":"Action 3"},
{"key":15, "location":"225 500", "step":"15", "text":"Action 4"},
{"key":16, "category":"Exclusive", "location":"300 550"},
{"key":17, "location":"300 600", "step":"17", "text":"Action 6"},
{"key":21, "location":"500 50", "step":"21", "text":"Act 1"},
{"key":22, "location":"500 100", "step":"22", "text":"Act 2"},
{"key":23, "location":"500 150", "step":"23", "text":"Act 3"},
{"key":24, "location":"500 200", "step":"24", "text":"Act 4"},
{"key":31, "location":"500 400", "step":"31", "text":"Act 1"},
{"key":32, "location":"500 450", "step":"32", "text":"Act 2"},
{"key":33, "location":"500 500", "step":"33", "text":"Act 3"},
{"key":34, "location":"500 550", "step":"34", "text":"Act 4"}
 ],
  "linkDataArray": [
{"from":1, "to":2, "text":"condition 1"},
{"from":2, "to":3},
{"from":2, "to":4},
{"from":3, "to":5, "text":"condition 2"},
{"from":4, "to":6},
{"from":5, "to":6},
{"from":6, "to":7, "text":"condition 5"},
{"from":11, "to":12, "text":"condition 1"},
{"from":12, "to":13, "text":"condition 12"},
{"from":12, "to":14, "text":"condition 13"},
{"from":13, "to":15, "text":"condition 2"},
{"from":14, "to":16, "text":"condition 14"},
{"from":15, "to":16, "text":"condition 15"},
{"from":16, "to":17, "text":"condition 5"},
{"from":21, "to":22, "text":"c1"},
{"from":22, "to":23, "text":"c2"},
{"from":23, "to":24, "text":"c3"},
{"from":21, "to":24, "text":"c14", "category":"Skip"},
{"from":31, "to":32, "text":"c1"},
{"from":32, "to":33, "text":"c2"},
{"from":33, "to":34, "text":"c3"},
{"from":33, "to":32, "text":"c14", "category":"Repeat"}
 ]}
  </textarea>
</div>
</body>
</html>
